package scanner;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

public class Scanner {
	
	private PushBackQueue reader;
	private boolean closed =false, started = false;
	
	public Scanner(String s)
	{
		this(new StringReader(s));
	}
	
	public Scanner (File file) throws FileNotFoundException
	{
		this(new FileReader(file));
	}
	
	public Scanner (Reader r)
	{
		reader = new PushBackQueue(r);
	}

	public void pushBack(String s)
	{
		for(int i = s.length()-1; i <= 0; i++)
			reader.pushBack((int)s.charAt(i));
	}
	
	public Token getToken() throws IOException
	{
	    if(!started)
	    {
	        started = true;
	        return new Token("",TokenType.START);
	    }
	    
	    if(closed)
	        return null; //if we've already reached eof don't try to read
	    
		int c;
		while((c = reader.read()) != -1)
		    //throw away white space(won't be in quoted string)
            if (("" + (char) c).matches("\\s"))
                continue;
            else break;
	
	    if (c!= -1)
	    {
	        char a = (char) c;
	        //System.err.println(a + "");
	        TokenType type = null;
    	    
    	    //take care of our special chars early
    	    if((type = TokenType.special("" + a)) != null)
                return new Token(a + "", type);
    	    
    	    Token t = null;
    	    
    	    switch(a)
    	    {
        	    case 'B': //BEGIN
        	    	reader.pushBack(c);
        	    	return expect(TokenType.BEGIN);
        	    case 'E': //END or EOL
        	    	c = reader.read();
    	            switch(c)
    	            {
        	            case (int) 'O':
        	            	reader.pushBack((int)'O');
        	            	reader.pushBack((int)'E');
        	            	return expect(TokenType.EOL);
        	            case (int) 'N':
        	            	reader.pushBack((int)'N');
        	            	reader.pushBack((int)'E');
        	            	return expect(TokenType.END);
        	            default:
        	                //error token, pushback character
        	                reader.pushBack(c);
        	                return new Token("E",TokenType.ERROR);
    	            }
        	    case 'p':  //print
        	    	reader.pushBack(c);
        	    	return expect(TokenType.PRINT);
        	    case 'i':  //print
        	    	reader.pushBack(c);
        	    	return expect(TokenType.INSERT);
        	    case 's': //substring
        	    	reader.pushBack(c);
        	    	return expect(TokenType.SUBSTRING);
        	    case 'r': //replace or remove
        	    	c = reader.read();
        	    	if(c != 'e')
        	    	{
        	    		reader.pushBack(c);
	                	return new Token("r",TokenType.ERROR);
        	    	}
        	    	c = reader.read();
    	            switch(c)
    	            {
        	            case (int) 'p':
        	            	reader.pushBack((int)'p');
        	            	reader.pushBack((int)'e');
        	            	reader.pushBack((int)'r');
        	            	return expect(TokenType.REPLACE);
        	            case (int) 'm':
        	            	reader.pushBack((int)'m');
	    	            	reader.pushBack((int)'e');
	    	            	reader.pushBack((int)'r');
        	            	return expect(TokenType.REMOVE);
        	            default:
        	                //error token, pushback character
        	                reader.pushBack(c);
        	                return new Token("E",TokenType.ERROR);
    	            }
        	    case 'l':  //length
        	    	reader.pushBack(c);
        	    	t = expect(TokenType.LENGTH);
        	        return new Token(t.getTokenString() + " - Variables," + 
        	        		" capture groups and the length function are" +
        	        		" not supported.", TokenType.ERROR);
        	    case 'w': //while
        	    	reader.pushBack(c);
        	    	return expect(TokenType.WHILE);
        	    case 'L':  //LINE
        	    	reader.pushBack(c);
        	    	return expect(TokenType.LINE);
        	    case 'f':  //for
        	    	reader.pushBack(c);
        	    	return expect(TokenType.FOR);
        	    case 'a':  //char
        	    case 'b':
        	    case 'c':
        	    	return new Token(a + "", TokenType.CHARACTER);
        	    case '"':
        	    	reader.pushBack(c);
        	    	return expectQuotedString();
        	    case '$'://TODO should push back $, then let expect var do it...
        	        t = expectVariable();
        	        return new Token(t.getTokenString() + " - Variables," + 
        	        		" capture groups and the length function are" +
        	        		" not supported.", TokenType.ERROR);
        	    default:
        	        //if numeric, follow int path
        	    	if(Character.isDigit(a))
        	    	{
        	    		reader.pushBack(c);
        	    		return expectInteger();
        	    	}
        	    	else if(a == '#')
        	    		return new Token(t.getTokenString() + " - Variables," + 
        	        		" capture groups and the length function are" +
        	        		" not supported.", TokenType.ERROR);
        	    	//otherwise error token, swallow
        	    	else
        	    		return new Token(a + "", TokenType.ERROR);
    	    }
	    }

		closed = true;
	    return null;
	}
	
	/*
	 * Expect functions- read as far as the output is what you expect.  When it
	 *  isn't, push any characters that don't fit back into the queue, and
	 * return an error token with those up until our non-fit.
	 * 
	 * Roughly, the scanner should look for hints to what the following token
	 * should be, pushback the character it read, then call the appropriate
	 * expect function.
	 * 
	 * Expect functions will return an error token if they don't find the
	 * expected input- even if that input could be another valid token.  
	 */
	
	//the basic expect function requires strict (ie literal) token types-
	//passing it VARIABLE would always return a variable token of "".
	private Token expect(TokenType type) throws IOException
	{
	    StringBuffer buf = new StringBuffer(type.getStringRep()),
	    		acc = new StringBuffer();
	    int c;
	    
	    //System.err.println(buf);
	    
	    while(buf.length() > 0)
	    {
	    	c = reader.read();
	    	//System.err.println((char) c + "");
	    	if(  c != buf.charAt(0))
	    	{
	    		String err = acc.toString();
	    		if (c != -1 )
	    		{
	    			reader.pushBack(c);
	    			err += (char)c;
	    		}
	    		return new Token(err, TokenType.ERROR);//return error token
	    	}
	    	
	    	buf.deleteCharAt(0);
	    	acc.append((char) c);
	    }
	    
	    return new Token(type.getStringRep(), type);
	}
	
	private Token expectQuotedString() throws IOException
	{
		int c = reader.read();
	    StringBuffer acc = new StringBuffer();
	    
	    if(c == -1 || c != '"')
	    {
	    	reader.pushBack(c);
	    	return new Token("", TokenType.ERROR);
	    }
	    
	    acc.append((char) c);
	    
	    do
	    {
	    	c = reader.read();
	    	if(c == -1)
	    	{
	    		reader.pushBack(c);
	    		return new Token(acc.toString(), TokenType.ERROR);
	    	}
	        acc.append((char)c);
	    }
	    while((char)c != '"');
	    
	    return new Token(acc.toString(), TokenType.QUOTED_STRING);
	}
	 
	
	private Token expectInteger() throws IOException 
	{
	    int c = reader.read();
	    StringBuffer acc = new StringBuffer();
	    
	    while(c != -1 && Character.isDigit(c))
	    {
	        acc.append((char)c);
	        c = reader.read();
	    }
	    
	    reader.pushBack(c);
	    if(acc.length() == 0)
	    	return new Token("", TokenType.ERROR);
	    
	    return new Token(acc.toString(), TokenType.INTEGER);
	}
	
	
	//expects one or more digits from the queue
	private Token expectVariable() throws IOException 
	{
	    Token t = expectInteger();
	    t.setTokenString("$" + t.getTokenString());
	    t.setType(TokenType.VARIABLE);
	    return t;
	}
}